Skip to content

feat: make PlayerInfoPanel available in replays#3855

Open
mike-s-zaugg wants to merge 3 commits into
openfrontio:mainfrom
mike-s-zaugg:feature/playerpanel-in-replay
Open

feat: make PlayerInfoPanel available in replays#3855
mike-s-zaugg wants to merge 3 commits into
openfrontio:mainfrom
mike-s-zaugg:feature/playerpanel-in-replay

Conversation

@mike-s-zaugg
Copy link
Copy Markdown
Contributor

@mike-s-zaugg mike-s-zaugg commented May 6, 2026

Description:

Makes the PlayerInfoPanel accessible during replay playback, as suggested by @FloPinguin.

Previously the panel returned empty immediately when myPlayer() was null (which is always the case in replays since the viewer is not an active participant). The panel now renders in a read-only mode showing all
informational content: player name, flag, resources, alliances, betrayal count, and trading status.

The right-click radial menu was also gated on myPlayer !== null, so it never opened in replays. In replay mode the radial now opens with only the info wedge enabled when right-clicking on a player tile, which is the
entry point to the panel.

Action buttons that require sending intents to the server are hidden in replay mode since they are not applicable:

  • Chat, Emoji, Target
  • Donate Troops / Donate Gold
  • Alliance Request / Break Alliance
  • Embargo / Stop-Trade buttons
  • Moderation (Kick)
  • Rocket direction toggle

Read-only content (resources, alliances, stats) remains fully visible, which was the main motivation for this feature: being able to review alliances during a replay.

Screenshots

Screenshot_17

Radial menu only shows info option.

Screenshot_16

Displays all read-only content.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Tested locally with npm run dev:prod against a real prod replay (/w1/game/tvBd246s?live).

Please put your Discord username so you can be contacted if a bug or regression is found:

sxndrexe

Show player info (resources, alliances, stats) when clicking on a player
during replay playback. Action buttons are hidden as they are not applicable
in replay mode.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 6, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Review Change Stack

Walkthrough

This PR adds spectator mode support to the player UI and radial menu system. It introduces a central isSpectator() detection method, routes spectator-safe rendering through a read-only viewer fallback, gates interactive features behind spectator checks, and makes menu handlers defensive against null player context during replay.

Changes

Spectator mode support

Layer / File(s) Summary
Spectator detection contract
src/core/game/GameView.ts
New isSpectator() method returns true when the current player is not alive or the game is in replay mode.
Radial menu spectator handling
src/client/graphics/layers/MainRadialMenu.ts
Right-click handler detects spectator mode and shows info-only radial with empty actions; tick() returns early when player is null; chat modal conditional simplified.
Player panel spectator rendering
src/client/graphics/layers/PlayerPanel.ts
Render method detects spectator mode, uses viewer = my ?? other for read-only identity and stats display, and gates interactive features (moderation, actions, rocket toggle) behind non-spectator checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

📺 When players fade to spectral ghosts,
The panel bows to read-only posts,
No actions fly, no rockets soar,
Just viewer shows the game's great lore. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: making PlayerInfoPanel accessible in replays, which matches the core objective of the PR.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the motivation, implementation approach, and user-facing changes with screenshots.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/graphics/layers/PlayerPanel.ts (1)

954-990: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block intent-capable modals in replay mode as well

Line 954 and Line 975 still allow modal rendering in replay when state is already set. Those modals can emit intent events, so replay is not fully read-only unless these branches are also gated by replay mode.

Suggested patch
+    const canInteract = !isReplay;
...
-                    ${this.sendTarget
+                    ${canInteract && this.sendTarget
                       ? html`
                           <send-resource-modal
...
-                    ${this.moderationTarget
+                    ${canInteract && this.moderationTarget
                       ? html`
                           <player-moderation-modal
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/graphics/layers/PlayerPanel.ts` around lines 954 - 990,
PlayerPanel still renders intent-capable modals when sendTarget or
moderationTarget are set even in replay mode; update the template branches that
render <send-resource-modal> and <player-moderation-modal> to also check the
replay flag and skip rendering when in replay. Locate the modal conditional
expressions referencing this.sendTarget (and this.sendMode) and
this.moderationTarget inside PlayerPanel and add a guard such as &&
!this.isReplay (or the project's replay flag, e.g., this.g.isReplay) so the
modals are not instantiated in replay/read-only mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/client/graphics/layers/PlayerPanel.ts`:
- Around line 954-990: PlayerPanel still renders intent-capable modals when
sendTarget or moderationTarget are set even in replay mode; update the template
branches that render <send-resource-modal> and <player-moderation-modal> to also
check the replay flag and skip rendering when in replay. Locate the modal
conditional expressions referencing this.sendTarget (and this.sendMode) and
this.moderationTarget inside PlayerPanel and add a guard such as &&
!this.isReplay (or the project's replay flag, e.g., this.g.isReplay) so the
modals are not instantiated in replay/read-only mode.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38b69f60-1712-45f5-a37b-168a4f44109a

📥 Commits

Reviewing files that changed from the base of the PR and between b8a544a and 24974e6.

📒 Files selected for processing (1)
  • src/client/graphics/layers/PlayerPanel.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 6, 2026
@FloPinguin
Copy link
Copy Markdown
Contributor

Please test it locally by running npm run dev:prod

You can watch all prod replays that way

And a screenshot would be nice then

@FloPinguin FloPinguin modified the milestone: v32 May 6, 2026
@FloPinguin
Copy link
Copy Markdown
Contributor

And prettier is failing, run npm run format

The previous commit made PlayerPanel render when myPlayer is null, but
the panel is only opened from the right-click radial menu, which itself
short-circuited on myPlayer === null. So the feature was unreachable in
replays.

- MainRadialMenu: in replay mode, skip the server actions() lookup and
  open the radial with empty PlayerActions on player tiles only
- RadialMenuElements: widen MenuElementParams.myPlayer to PlayerView | null;
  root submenu returns [info] only when myPlayer is null; guard
  deleteUnit cooldown/disabled against null

Also applies prettier to PlayerPanel.ts (CI was failing on it).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/graphics/layers/MainRadialMenu.ts`:
- Around line 118-126: The issue is that this.clickedTile is accessed inside
async callbacks in the methods around lines 118 and 188, leading to stale or
incorrect tile usage if it changes before the promise resolves. Fix this by
capturing the current clickedTile value into a local constant before calling
myPlayer.actions or other async functions, then use that local constant inside
the callback to update the player actions consistently for the intended tile.

In `@src/client/graphics/layers/PlayerPanel.ts`:
- Around line 1015-1022: renderAllianceExpiry() is still rendered when isReplay
is true, causing stale expiry text to persist because replay doesn't update the
live-only value; stop rendering or clear the underlying state when in replay:
update the template where renderAllianceExpiry() is used (near the current
conditional around renderActions) so it is not called when isReplay is true, and
additionally ensure show() and/or tick() detect entering replay mode and clear
the alliance-expiry state used by renderAllianceExpiry() (or set it to an
empty/undefined value) to be extra safe; reference renderAllianceExpiry(),
renderActions(), show(), and tick() when making the changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 323978f8-bb58-48ca-92b2-6972e512069b

📥 Commits

Reviewing files that changed from the base of the PR and between 24974e6 and 3fdcea9.

📒 Files selected for processing (3)
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/layers/PlayerPanel.ts
  • src/client/graphics/layers/RadialMenuElements.ts

Comment thread src/client/graphics/layers/MainRadialMenu.ts Outdated
Comment thread src/client/graphics/layers/PlayerPanel.ts
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 7, 2026
@mike-s-zaugg
Copy link
Copy Markdown
Contributor Author

mike-s-zaugg commented May 7, 2026

@FloPinguin I tested it localy and needed to apply some changes.

I added the features and screenshots.
Should I implement the propsed fixes from coderabbit aswell?

@FloPinguin
Copy link
Copy Markdown
Contributor

Thank you, I think it would make sense to just show the info panel on right click, skip the info-only-radial?

Yeah please check if rabbit is correct, sometimes it has good ideas / finds bugs, you can reply to its comments

Comment on lines 100 to 102
if (myPlayer === null && !isReplay) {
return;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so in this case we haven't spawned yet?


const my = this.g.myPlayer();
if (!my) return html``;
const isReplay = this.g.config().isReplay();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it maybe be cleaner to create:

this.g.isSpectator()

and it just does:

isSpectator() {
!(this.myPlayer()?.isAlive) || this.config.isReplay
}

const my = this.g.myPlayer();
if (!my) return html``;
const isReplay = this.g.config().isReplay();
if (!my && !isReplay) return html``;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so in this case we are in a live match but haven'ts spawned in? so we don't render the player panel?


<!-- Actions -->
${this.renderActions(my, other)}
${isReplay
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, instead of isReplay, it's isSpectator because being dead is the equivalent of watching a replay

evanpelle pointed out that being dead is basically the same as
watching a replay (and so is pre-spawn). Added game.isSpectator()
to cover all three and replaced the isReplay checks in PlayerPanel.

FloPinguin suggested skipping the info-only radial and just opening
the PlayerPanel directly on right-click. That let us revert all the
null-threading in RadialMenuElements.

Also fixed the stale clickedTile coderabbit flagged, and gated the
send-resource and moderation modals so they can't render for spectators.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/graphics/layers/MainRadialMenu.ts`:
- Around line 179-184: The early return when myPlayer is null leaves the radial
open; before returning from MainRadialMenu's logic, explicitly close the menu
(e.g. call this.close() or this.hide() on the radial instance) so
spectator/read-only mode is enforced; update the code around the myPlayer null
check (the block that uses this.clickedTile and calls this.updatePlayerActions)
to first close the radial then return.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6eec8e2-5cd9-440b-bbdc-ff5a7ac0cc63

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdcea9 and 686add7.

📒 Files selected for processing (3)
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/layers/PlayerPanel.ts
  • src/core/game/GameView.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/PlayerPanel.ts

Comment on lines +179 to +184
const myPlayer = this.game.myPlayer();
if (myPlayer === null) return;
const tile = this.clickedTile;
myPlayer.actions(tile).then((actions) => {
this.updatePlayerActions(myPlayer, actions, tile);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close stale radial when viewer becomes spectator.

At Line 180, early return keeps an already-open radial menu visible after spectator transition. Close it before returning so read-only mode is enforced consistently.

Proposed fix
   async tick() {
     if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return;
     const myPlayer = this.game.myPlayer();
-    if (myPlayer === null) return;
+    if (myPlayer === null || this.game.isSpectator()) {
+      this.closeMenu();
+      return;
+    }
     const tile = this.clickedTile;
     myPlayer.actions(tile).then((actions) => {
       this.updatePlayerActions(myPlayer, actions, tile);
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const myPlayer = this.game.myPlayer();
if (myPlayer === null) return;
const tile = this.clickedTile;
myPlayer.actions(tile).then((actions) => {
this.updatePlayerActions(myPlayer, actions, tile);
});
const myPlayer = this.game.myPlayer();
if (myPlayer === null || this.game.isSpectator()) {
this.closeMenu();
return;
}
const tile = this.clickedTile;
myPlayer.actions(tile).then((actions) => {
this.updatePlayerActions(myPlayer, actions, tile);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/graphics/layers/MainRadialMenu.ts` around lines 179 - 184, The
early return when myPlayer is null leaves the radial open; before returning from
MainRadialMenu's logic, explicitly close the menu (e.g. call this.close() or
this.hide() on the radial instance) so spectator/read-only mode is enforced;
update the code around the myPlayer null check (the block that uses
this.clickedTile and calls this.updatePlayerActions) to first close the radial
then return.

@mike-s-zaugg
Copy link
Copy Markdown
Contributor Author

@FloPinguin I have implemented your and evans suggestions.
Let me know if there is anything else I should change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

4 participants